package it.unicam.ids.pvm.postazione;

import it.unicam.ids.pvm.data.CassaDAO;
import it.unicam.ids.pvm.data.DAOException;
import it.unicam.ids.pvm.data.DAOFactory;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Iterator;

/**
 * La classe rappresenta il gestore del credito, sia di quello 
 * residuo nel distributore, che di quello appena immesso 
 * da un clienete. Contiene i seguenti attributi:
 * credito, credito immesso dal cliente per l'acquisto.
 * max_tempo, tempo massimo per l'azzeramento del credito.
 * tempo, tempo trascorso dall'inserimento del credito.
 * postazione, postazione di vendita a cui il gestore 
 * fa riferimento per l'acquisto.
 * cassa, cassa a cui fa riferimento il gestore per il denaro.
 * @author Luca Iachini, Valerio Iachini, Alessandro Cardenà
 *
 */
public class GestoreContanti implements Runnable {

	private double credito;
	public final long MAX_TEMPO = 180000; // TODO tempo azzeramento credito in
											// millisecondi
	private long tempo;
	private boolean running = false;
	private Thread tCredito;
	public PostazioneDiVendita postazione;
	Cassa cassa;
	CassaDAO cassaDAO;

	/**
	 * Costruttore della classe.
	 * Inizializza la postazione di vendita, il credito 
	 * e la cassa.
	 * @param postazione.
	 * @throws DAOException.
	 */
	
	public GestoreContanti(PostazioneDiVendita postazione) throws DAOException {
		this.postazione = postazione;
		credito = 0;
		DAOFactory df = DAOFactory.getDAOFactory();
		cassaDAO = df.getCassaDAO();
		cassa = cassaDAO.findByLato(postazione.getLato());
		if (cassa == null) {
			cassa = new Cassa(new int[Contante.values().length]);
			cassaDAO.insert(cassa, postazione.getLato());
		}
	}
	
	private ArrayList<AzzeraCreditoEventListener> _listeners = new ArrayList<AzzeraCreditoEventListener>();

	public synchronized void addEventListener(AzzeraCreditoEventListener listener) {
		_listeners.add(listener);
	}

	public synchronized void removeEventListener(AzzeraCreditoEventListener listener) {
		_listeners.remove(listener);
	}

	private synchronized void fireEventAzzeramentoCredito() {
		Iterator<AzzeraCreditoEventListener> i = _listeners.iterator();
		while (i.hasNext()) {
			((AzzeraCreditoEventListener) i.next()).azzeramentoCredito(new CreditoEvent(this, postazione.getLato()));
		}
	}

	/**
	 * Il metodo consente di inserire del contante nella cassa, 
	 * aggiornando la relativa tabella del database. 
	 * Se è inserita una chiavetta, essa viene ricaricata, 
	 * altrimenti si aggiorna il credito.
	 * @param contante contante immesso.
	 * @throws Exception.
	 */
	
	public synchronized void inserisciContante(Contante contante)
			throws Exception {
		startTempo();
		cassa.inserisciContante(contante, 1);
		if (postazione.getLettoreChiavetta().isInserita())
			postazione.getLettoreChiavetta().addCredito(contante.valore());
		else
			credito += contante.valore();
		cassaDAO.update(cassa, postazione.getLato());
	}
	
	/**
	 * Il metodo consente di pagare il prodotto acquistato, 
	 * aggiornando il credito.
	 * @param prezzo prezzo del prodotto.
	 * @throws CreditoInsufficienteException.
	 */

	public synchronized void paga(double prezzo)
			throws CreditoInsufficienteException {
		if (credito >= prezzo) {
			credito -= prezzo;
		} else
			throw new CreditoInsufficienteException();
	}

	/**
	 * Il metodo consente di erogare il resto al cliente.
	 * Il resto viene passato come un array di monete o 
	 * banconote e la relativa quantità. Viene aggiornata 
	 * la cassa e la relativa tabella del database.
	 * @return resto.
	 * @throws Exception.
	 */
	
	public int[] rilasciaResto() throws Exception {
		int nContante[] = new int[Contante.values().length];
		for (Contante c : Contante.values()) {
			int quantita = cassa.getQuantitaContante(c);
			nContante[c.posizione()] = (int) (credito / c.valore());
			if (nContante[c.posizione()] > quantita) {
				nContante[c.posizione()] = quantita;
				credito -= c.valore() * quantita;
			} else {
				credito %= c.valore();
			}
			cassa.ritiraContante(c, nContante[c.posizione()]);
			credito = round(credito, 2);
			if (credito == 0) {
				break;
			}
		}
		cassaDAO.update(cassa, postazione.getLato());
		return nContante;
	}

	/**
	 * Il metodo imposta la cassa da gestire.
	 * @param cassa.
	 */
	
	public void setCassa(Cassa cassa) {
		this.cassa = cassa;
	}

	/**
	 * Il metodo restituisce il credito immesso.
	 * @return credito.
	 */
	
	public double getCredito() {
		return credito;
	}
	
	/**
	 * Il metodo consente di azzerare il credito immesso.
	 */

	public void azzeraCredito() {
		credito = 0;
	}
	
	/**
	 * Il metodo permette di risolvere il problema 
	 * dell'errore macchina nell'arrotondamento del credito.
	 * @param d cifra da arrotondare.
	 * @param decimalPlacecifre decimali significative.
	 * @return cifra valore arrotondato.
	 */

	private double round(double d, int decimalPlace) {
		BigDecimal bd = new BigDecimal(Double.toString(d));
		bd = bd.setScale(decimalPlace, BigDecimal.ROUND_HALF_UP);
		return bd.doubleValue();
	}

	/**
	 * Il metodo consente di far partire il conto alla rovescia 
	 * per l'azzeramento del denaro.
	 */
	
	private void startTempo() {
		tempo = MAX_TEMPO;
		if (!running) {
			running = true;
			tCredito = new Thread(this);
			tCredito.start();
		}
	}
	
	/**
	 * Il metodo consente di fermare il conto alla 
	 * rovescia per l'azzeramento del credito.
	 */

	private void stopTempo() {
		running = false;
	}
	
	@Override
	public void run() {
		while (running) {
			try {
				Thread.sleep(1000);
				tempo -= 1000;
			} catch (InterruptedException e) {
			}
			synchronized (this) {
				if (tempo <= 0) {
					azzeraCredito();
					fireEventAzzeramentoCredito();
					stopTempo();
				}
			}
		}
	}
}